const ws = require('nodejs-websocket');
const encrypt = require('./encrypt');
const config = require('./config.js');
const _ = require('lodash');
const redis = require('./redis');
// userConn只是保存socket，数据结构为：aaa: socket 连接对象
const userConn = {};
// 保存用户的随机串，只有当用户重新登录时才会更新，socket断开连接和它没关系
// 数据结构为：aaa: 随机串，已改为 redis
// const userToken = {};
/*
该对象是记录普通用户的观看记录的，超过指定数量不能继续观看
userViewRecord = {
  aaa:{
    videoHistory: ['apt-135'],
    expireDate: '2021 02 01'
  }
}
*/
// const userViewRecord = {} // 使用 redis 代替
/*
userVideoDurationRecord = {
  如果是普通(aaa)、购买时长(bbb)用户，每打开一个视频都会生成一个键值对
  普通用户扣减对应视频的，购买时长用户是为了记录是否快进，并扣减用户名为key的值
  aaa: {
    abp-135: {
      duration: 300,
      playFile: 'playlist20.ts'
    }
  },
  bbb: {
    购买时长的用户只扣减这一个
    duration: 1000
    abp-135: {
      playFile: 'playlist20.ts'
    }
  }
}
*/
// const userVideoDurationRecord = {} // 使用 redis 代替

const server = ws.createServer(connection => {
  // result 可能为：
  // (1) Add:/aaa/随机串
  // (2) Auth:/aaa/随机串
  // (3) /aaa/随机串
  connection.on('text', async function (result) {
    // result = getTsFile:/aaa/encrypt(1:aaa):a/playlist0.ts
    if (result.startsWith('getTsFile:')) {
      // splitedResult = ["getTsFile", /aaa/encrypt(memberStatus:userName), 'a/playlist0.ts']
      const splitedResult = result.split(':');
      const videoNameAndPlaylist = splitedResult[2].split('/')
      const roleAndUsername = encrypt.rsaPublicDecrypt(decodeURIComponent(splitedResult[1].split('/').filter(item => item)[1]).replace(/\<\|\>/g, '/')).toString()
      // splitedRoleAndUsername = [memberStatus, userName]
      const splitedRoleAndUsername = roleAndUsername.split(':')
      
      // 如果是开头那一小段视频，直接返回，毕竟要加载封面
      if (splitedResult[2].endsWith('playlist0.ts')) {
        await redis.hSet(splitedRoleAndUsername[1] + videoNameAndPlaylist[0] + '-durationAndPrevTs', 'playFile', 'playlist0.ts')
        connection.send('getTsFile:' + splitedResult[2]);
        return;
      }

      // 先看有没有该用户的token，比对存起来的 token 和传过来的 encrypt(memberStatus:userName) 是否相同，也就是让被挤出去的用户不能再看
      const userToken = await redis.get(splitedRoleAndUsername[1] + '-token')
      if (userToken == undefined || userToken !== splitedResult[1].split('/').filter(item => item)[1]) {
        connection.send('getTsFile:false')
        return;
      }

      // 如果是普通用户
      if (config.normalMember == splitedRoleAndUsername[0]) {
        // 如果超过免费观看数量
        const userViewRecordLen = await redis.sCard(splitedRoleAndUsername[1] + '-viewRecord')
        if (userViewRecordLen > config.normalMemberCanViewNumber ) {
          connection.send('getTsFile:false');
          return;
        }
        // 如果是普通用户，该视频观看时长没超过体验时间
        let duration = await redis.hGet(splitedRoleAndUsername[1] + videoNameAndPlaylist[0] + '-durationAndPrevTs', 'duration')
        duration -= 0;
        if ( duration < config.normalMemberCanViewDuration ) {
          // 如果用户快进或者倒退进度条，那么播放器加载的下一个6秒ts视频不计时，需要把加的那6秒减了，就是把预加载的那6秒的视频段，给替换成 他快进或者倒退后要加载的那段视频
          // 获取用户最后一个ts文件 比如 playlist20.ts
          const lastFile = await redis.hGet(splitedRoleAndUsername[1] + videoNameAndPlaylist[0] + '-durationAndPrevTs', 'playFile')
          const userPlayFile = lastFile || 'playlist0.ts';
          // 匹配数字，匹配当前请求的 ts 文件里的数字 和 最后一次请求的 ts 文件的数字，进行相减
          // 当前请求的这个数字小于最后请求的都数字，则说明倒退进度条，大于1，则说明快进进度条
          if (videoNameAndPlaylist[1].match(/\d+/)[0] - userPlayFile.match(/\d+/)[0] != 1) {
            console.log('快进----减时长前', duration)
            await redis.hSet(splitedRoleAndUsername[1] + videoNameAndPlaylist[0] + '-durationAndPrevTs', 'duration', duration -= config.videoDefaultDuration)
            console.log('快进----减时长后', duration)
          }
          // 加时长并记录这个视频的播放段
          await redis.hSet(splitedRoleAndUsername[1] + videoNameAndPlaylist[0] + '-durationAndPrevTs', 'duration', duration += config.videoDefaultDuration)
          console.log('正常----加时长后', duration)
          await redis.hSet(splitedRoleAndUsername[1] + videoNameAndPlaylist[0] + '-durationAndPrevTs', 'playFile', videoNameAndPlaylist[1])

          await redis.multi().sAdd(splitedRoleAndUsername[1] + '-viewRecord', videoNameAndPlaylist[0])
            .expire(splitedRoleAndUsername[1] + '-viewRecord', Math.floor(getRemainingSeconds())).exec();
          connection.send('getTsFile:' + splitedResult[2]);
          return;
        }
        // 走到这就是超过了
        connection.send('getTsFile:false');
        return;
      }

      // 如果是购买时长用户
      if (config.durationMember == splitedRoleAndUsername[0]) {
        
        return;
      }

      return;
    }

    // 4、添加用户的 token
    if (result.startsWith('Add:')) {
      result = result.replace(/Add:/, '')
      // account = ['', aaa, encrypt(memberStatus:userName)]
      const account = result.split('/');
      // req.cookies.authId = /username/encrypt(memberStatus:userName)
      const encryptRoleAndUsername = account[account.length - 1]
      // roleAndUsername = memberStatus:userName:timestamp
      const roleAndUsername = encrypt.rsaPublicDecrypt(decodeURIComponent(encryptRoleAndUsername).replace(/\<\|\>/g, '/')).toString()
      const roleAndUsernameTimestamp = roleAndUsername.split(':')

      // 5、如果在 userConn 里找到该账号的连接，说明账号正在另一个设备上登录，那么就把另一个设备踢出，并删掉账号的连接和 另一个设备登录时生成的token
      // 直接用明文是因为Add:开头的消息是controller\index.js login 函数发过来的，可以信任
      if (Object.keys(userConn).includes(roleAndUsernameTimestamp[1])) {
        userConn[roleAndUsernameTimestamp[1]].send('该账号已在其它设备登录，你已被踢出');
        userConn[roleAndUsernameTimestamp[1]].close();
        delete userConn[roleAndUsernameTimestamp[1]];
        await redis.del(roleAndUsernameTimestamp[1] + '-token')
      }

      // 6、把该账号的 token 改为这次登录时生成的 token，过期时间是24小时，与 cookie 一样
      await redis.set(roleAndUsernameTimestamp[1] + '-token', encryptRoleAndUsername, {
        EX: 86400
      })
      return;
    }

    // 10、看这个 token 有没有存在，如果是被挤出的A用户访问，由于该账号的 token 已经被B登录时刷新了，A用户的 token 是找不到的，返回验证错误
    // 如果是 B用户登录成功，前端触发刷新，那么这里是返回验证成功
    const isStartsWithAuth = result.startsWith('Auth:');
    result = result.replace(/Auth:/, '')
    // account = ['', aaa, encrypt(memberStatus:userName)]
    const account = result.split('/');
    const encryptRoleAndUsername = account[account.length - 1]
    // roleAndUsername = memberStatus:userName:timestamp
    const roleAndUsername = encrypt.rsaPublicDecrypt(decodeURIComponent(encryptRoleAndUsername).replace(/\<\|\>/g, '/')).toString()
    const roleAndUsernameTimestamp = roleAndUsername.split(':')


    // 14、访问文件时的验证，只返回 true 或 false，被挤出的A用户，带着被删除的 token 访问，肯定返回 false，只有新登录的 B用户，带着新生成的 token 才会返回 true
    const userToken = await redis.get((roleAndUsernameTimestamp[1] || '') + '-token')
    if (userToken == encryptRoleAndUsername) {
      connection.send(`${isStartsWithAuth ? 'Auth:' : ''}true`)
      return true;
    }
    connection.send(`${isStartsWithAuth ? 'Auth:' : ''}false`)
    return false;
  });

  connection.on('close', async function (code) {
    if (connection.path == '/') return;
    const path = connection.path.split('/').filter(item => item)
    const roleAndUsername = encrypt.rsaPublicDecrypt(decodeURIComponent(path[1]).replace(/\<\|\>/g, '/')).toString()
    // splitMemberStatus: [memberStatus, userName]
    const splitedRoleAndUsername = roleAndUsername.split(':')
    // 17、当前端用户离开时，会触发 close 事件，视为关闭浏览器下线，删掉这个 socket 连接，但是不删除 随机串
    // 随机串的替换和删除只有在 on text 事件中，消息为 Add: 开头时才会触发，触发情况一般是：
    // (1) 用户删除浏览器缓存，清除了 cookie 中的 authId，给他返回了登录页，他从新登录
    // (2) A用户登录了 aaa 账号，B用户也登录 aaa 账号，B用户的随机串把A用户的给覆盖了
    // 所以只要没有人挤号，A用户 cookie 中 authId 也永远在就不会删除这个 token
    // 哪怕 aaa 这个号一年都没有被登录，userToken 中 aaa 用户的 token 都是一直在的
    // 要想实现一天、一周、一个月强制用户重新登录，那么就设置 cookie 的有效期，不过容易被篡改
    // 另一种解决办法是设置一个定时器，去定时删除 userToken 中的 token，也可以放到 redis 中 设置过期时间
    delete userConn[splitedRoleAndUsername[1]]
  });

  connection.on('error', function (code) {
  })
}).listen(36013);

// 当有客户端连接时，会触发该事件。
// result 和 ws.createServer 产生的 connection 是同一个对象，它们有一个close函数，调用就可以让服务端主动关闭这个socket
server.on("connection", async function (result) {
  // 16、首页会建立 socket 连接，把该用户的 socket 连接保存起来，path = / 是后端启动时 serve-static 连接的，不用保存

  // 一般来说 result.path = /aaa/encrypt(memberStatus:userName)
  if (result.path == '/') return;
  // path = [aaa，encrypt(memberStatus:userName)]
  const path = result.path.split('/').filter(item => item)
  // memberStatus = "memberStatus:userName"
  const memberStatus = encrypt.rsaPublicDecrypt(decodeURIComponent(path[1]).replace(/\<\|\>/g, '/')).toString()
  // splitMemberStatus: [memberStatus, userName]
  const splitMemberStatus = memberStatus.split(':')

  userConn[splitMemberStatus[1]] = result;

});

// 前提：前端带着用户名和随机字符串发送 socket 请求
// 用户首次登录，保存账号和随机字符串到 userConn   假设是 /zhangsan/a
// 同样的账号被另一个用户登录   假设是 /zhangsan/b
// 先根据 zhangsan 从 userConn 找到已登录的 /zhangsan/a，由于 value 就是 connection，直接给客户端发送被踢出的消息
// 然后把 /zhangsan/a 从 userConn 中删除，替换为 /zhangsan/b，实现踢出的操作
// 仅仅只有提示还不够，如果用户删除掉 websocket 代码，直接调用接口，或者直接输入视频地址，例如：http://xxx.xxx.xxx.xxx:3001/a.mp4
// 所以就需要对 express 的静态目录进行二次开发
//                                                                      authId 就是前端生成的用户名 + 随机字符串 = /zhangsan/a
// 大概思路就是，先创建一个 websocket 对象，每次请求过来，都根据 cookie 里的 authId 去找 socket 验证，socket 从 userConn 中找 authId
// 来返回 true 或 false



// 为什么要加随机字符串？
// 比如用户名是 zhangsan，A用户 是被 B用户挤出去了，但是A用户把 cookie authId 设置成 zhangsan，然后A用户直接输入视频地址，跳过了websocket的连接过程
// 然后静态目录去找 socket 验证，发现 zhangsan 是存在的，于是便返回视频文件
// 没有达到踢出的效果，所以要有一个东西来区分不同的设备

// 随机字符串是加密的，md5(loginName + 年-月-日, qwezxc)
// qwezxc是盐

function getRemainingSeconds() {
  var now = new Date();
  var end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59);
  var diff = end.getTime() - now.getTime();
  return diff / 1000;
}